/*
  This file is part of JCM.

  JCM is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  JCM is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with JCM.  If not, see <http://www.gnu.org/licenses/>.
*/
import java.math.MathContext;
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.util.Locale;

import static java.math.BigDecimal.ONE;
import static java.math.BigDecimal.TEN;

/**
 *  Representação de taxa de juros.
*/
public class InterestRate
{
  /** Valor da taxa de juros. */
  private BigDecimal valor;

  /** Período de aplicação da taxa de juros (AO_DIA, AO_MES, AO_ANO). */
  private int periodo;

  /** Regime de capitalização (linear = true, exponencial = false). */
  private boolean regime;

  /** Número efetivos de dias num mês comercial. */
  private int diasNoMes;

  /** Número efetivos de dias num ano comercial. */
  private int diasNoAno;

  /** Formatador de números com ponto flutuante. */
  private NumberFormat df;

  /**
   * Construtor padrão do objeto Taxa de Juros.
   *
   * @param Valor Valor da taxa de juros.
   * @param Periodo Período de aplicação da taxa de juros.
   * @param Regime regime de capitalização da taxa de juros.
   * @return Instância do objeto representando Taxa de juros.
  */
  public InterestRate(BigDecimal Valor, int Periodo, boolean Regime)
  {
    set(Valor, Periodo, Regime);

    diasNoMes = DIAS_NO_MES;
    diasNoAno = DIAS_NO_ANO;
    df = NumberFormatFactory.getDecimalFormat();
  }

  /**
   * Assinala todos os componentes da Taxa de Juros instânciada.
   *
   * @param Valor Valor da taxa de juros.
   * @param Periodo Período de aplicação da taxa de juros.
   * @param Regime regime de capitalização da taxa de juros.
  */
  public void set(BigDecimal Valor, int Periodo, boolean Regime)
  {
    this.valor = Valor;
    this.periodo = Periodo;
    this.regime = Regime;
  }

  /**
   * Assinala alguns componentes da Taxa de Juros instânciada.
   *
   * @param Valor Valor da taxa de juros.
   * @param Periodo Período de aplicação da taxa de juros.
  */
  public void set(BigDecimal Valor, int Periodo)
  {
    this.valor = Valor;
    this.periodo = Periodo;
  }

  /**
   * Assinala o valor da Taxa de Juros.
   *
   * @param Valor Valor da taxa de juros a assinalar.
  */
  public void setValor(BigDecimal Valor)
  {
    this.valor = Valor;
  }

  /**
   * Retorna o valor da Taxa de Juros.
   *
   * @return O valor da taxa de juros.
  */
  public BigDecimal getValor()
  {
    return this.valor;
  }

  /**
   * Assinala o período da Taxa de Juros.
   *
   * @param O período da taxa de juros a assinalar.
  */
  public void setPeriodo(int Periodo)
  {
    this.periodo = Periodo;
  }

  /**
   * Retorna o período da Taxa de Juros.
   *
   * @return O período da taxa de juros.
  */
  public int getPeriodo()
  {
    return this.periodo;
  }

  /**
   * Assinala o regime da Taxa de Juros.
   *
   * @param O regime da taxa de juros.
  */
  public void setRegime(boolean Regime)
  {
    this.regime = Regime;
  }

  /**
   * Retorna o regime da Taxa de Juros.
   *
   * @return O regime da taxa de juros.
  */
  public boolean getRegime()
  {
    return this.regime;
  }

  /**
   * Assinala o número de dias no mês a ser usado nas conversões por
   * mudanças no período e/ou regime de capitalização.
   * O valor default é DIAS_NO_MES = 30 dias.
   *
   * @param O número de dias no mês.
  */
  public void setDiasNoMes(int ndias)
  {
    diasNoMes = ndias;
  }

  /**
   * Assinala o número de dias no ano a ser usado nas conversões por
   * mudanças no período e/ou regime de capitalização.
   * O valor default é DIAS_NO_ANO = 360 dias.
   *
   * @param O número de dias no ano.
  */
  public void setDiasNoAno(int ndias)
  {
    diasNoAno = ndias;
  }

  /**
   * Calcula a taxa equivalente para o período fornecido.
   *
   * @param Periodo O perído alvo da conversão.
   * @return O valor da taxa equivalente para o período.
  */
  public BigDecimal eqValue(int Periodo)
  {
    if (this.periodo == Periodo) return valor;

    if (this.periodo == AO_DIA)
    {
      int x = (Periodo == AO_MES) ? diasNoMes : diasNoAno;
      if (this.regime == LINEAR_MODEL)
        return valor.multiply(new BigDecimal(x));
      else
        return valor.add(ONE).pow(x).subtract(ONE);
    }

    if (this.periodo == AO_MES)
    {
      if (Periodo == AO_ANO) {
        if (this.regime == LINEAR_MODEL)
          return valor.multiply(new BigDecimal(MESES_NO_ANO));
        else
          return valor.add(ONE).pow(MESES_NO_ANO).subtract(ONE);
      } else {
        if (this.regime == LINEAR_MODEL)
          return valor.divide(
            new BigDecimal(diasNoMes), MathContext.DECIMAL64);
        else
          return raise(valor.add(ONE), diasNoMes).subtract(ONE);
      }
    }

    // if (this.periodo == AO_ANO)
    int x = (Periodo == AO_MES) ? MESES_NO_ANO : diasNoAno;
    if (this.regime == LINEAR_MODEL)
      return valor.divide(new BigDecimal(x), MathContext.DECIMAL64);
    else
      return raise(valor.add(ONE), x).subtract(ONE);
  }

  /**
   * Calcula a taxa equivalente para o regime fornecido.
   *
   * @param Regime O regime alvo da conversão.
   * @return O valor da taxa equivalente para o regime.
  */
  public BigDecimal eqValue(boolean Regime)
  {
    if (this.regime == Regime) return this.valor;

    if (this.periodo == AO_ANO)
    {
      if (this.regime == LINEAR_MODEL) { /* equivalente EXPONENCIAL */
        BigDecimal value = valor.divide(
          new BigDecimal(MESES_NO_ANO), MathContext.DECIMAL64);
        return value.add(ONE).pow(MESES_NO_ANO).subtract(ONE);
      } else { /* equivalente LINEAR */
        BigDecimal value = raise(valor.add(ONE), MESES_NO_ANO).subtract(ONE);
        return value.multiply(new BigDecimal(MESES_NO_ANO));
      }
    }

    if (this.periodo == AO_MES)
    {
      if (this.regime == LINEAR_MODEL) { /* equivalente EXPONENCIAL */
        BigDecimal value = valor.multiply(new BigDecimal(MESES_NO_ANO));
        return raise(value.add(ONE), MESES_NO_ANO).subtract(ONE);
      } else { /* equivalente LINEAR */
        BigDecimal value = valor.add(ONE).pow(MESES_NO_ANO).subtract(ONE);
        return value.divide(
          new BigDecimal(MESES_NO_ANO), MathContext.DECIMAL64);
      }
    }

    // if (this.periodo == AO_DIA)
    if (this.regime == LINEAR_MODEL) { /* equivalente EXPONENCIAL */
      BigDecimal value = valor.multiply(new BigDecimal(diasNoMes));
      return raise(value.add(ONE), diasNoMes).subtract(ONE);
    } else { /* equivalente LINEAR */
      BigDecimal value = valor.add(ONE).pow(diasNoMes).subtract(ONE);
      return value.divide(new BigDecimal(diasNoMes), MathContext.DECIMAL64);
    }
  }

  /**
   * Instancia um objeto taxa de juros com valor equivalente para algum
   * regime fornecido.
   *
   * @param Regime O regime alvo de conversão.
   * @return A instância de taxa juros.
  */
  public InterestRate eqRate(boolean Regime)
  {
    return new InterestRate(eqValue(Regime), this.periodo, Regime);
  }

  /**
   * Instancia um objeto taxa de juros com valor equivalente para algum
   * período fornecido.
   *
   * @param Período O período alvo de conversão.
   * @return A instância de taxa juros.
  */
  public InterestRate eqRate(int Periodo)
  {
    return new InterestRate(eqValue(Periodo), Periodo, this.regime);
  }

  /**
   * Calcula a raiz de 'x^exponent - value' pelo método iterativo de
   * Newton-Raphson visando taxas equivalentes que envolvem regime de
   * capitalização composto.
   *
   * @param value O argumento da radiciação :: taxa de juros.
   * @param exponent O expoente inteiro da variável :: número de períodos.
   * @return A aproximação da raiz.
  */
  public BigDecimal raise(BigDecimal value, int exponent)
  {
    // valor do expoente como BigDecimal
    final BigDecimal e = new BigDecimal(exponent);

    BigDecimal d, f;

    // aproximação inicial da raíz usando tipo primitivo
    BigDecimal x = new BigDecimal( Math.pow(value.doubleValue(), 1d/exponent) );

    // calcula valor da função para a aproximação inicial
    BigDecimal g = x.pow(exponent).subtract(value);

    // valor do expoente da derivada
    int j = exponent - 1;

    // loop de iterações do método com critério de parada naive
    do {

      // assinala para f o valor prévio de g(x)
      f = g;

      // cálcula o valor da derivada de 1a ordem em x
      d = e.multiply( x.pow(j) );

      // cálcula o novo valor da raíz conforme método de Newton
      x = x.subtract( f.divide(d, MathContext.DECIMAL64) );

      // cálcula valor da função para o novo valor da raíz
      g = x.pow(exponent).subtract(value);

    } while (f.subtract(g).abs().compareTo(EPS) > 0);

    return x;
  }

  /** Valor default do máximo percentual representável. */
  private BigDecimal MAX = new BigDecimal("0.12");

  /**
   * Retorna o valor do máximo percentual representável.
   *
   * @return O valor do máximo percentual representável.
  */
  public BigDecimal getMAX() { return MAX; }

  /**
   * Assinala o valor do máximo percentual representável.
   *
   * @param A string do valor do máximo percentual representável.
  */
  public void setMAX(String max)
  {
    MAX = new BigDecimal(max);
  }

  /**
   * Testa se a taxa de juros é alternada para alguma taxa equivalente
   * se for maior que o valor máximo representável.
   *
   * @return O status de modificação da taxa de juros para uma taxa
   *         equivalente.
  */
  public boolean scaleOnGreaterThanMAX()
  {
    boolean modified = false;
    if (valor.subtract(MAX).abs().compareTo(EPS) <= 0) {
      valor = MAX;
    } else {
      int Periodo = periodo;
      while ((valor.compareTo(MAX) > 0) && (Periodo-- > AO_DIA))
      {
        valor = eqValue(Periodo);
        periodo = Periodo;
        modified = true;
      }
    }
    return modified;
  }

  /**
   * Retorna a representação textual da taxa sem o sufixo de percentual
   * e número arbitrário de digitos na parte fracionária.
  */
  public String asString(int ndigs)
  {
    df.setMinimumFractionDigits(ndigs);
    df.setMaximumFractionDigits(ndigs);
    return df.format(valor.multiply(HUNDRED));
  }

  /**
   * Retorna a representação textual da taxa sem o sufixo de percentual
   * e número default de digitos na parte fracionária.
   *
   * @return A representação textual da taxa sem o sufixo de percentual.
  */
  public String asString()
  {
    df.setMinimumFractionDigits(defMinDecimals);
    df.setMaximumFractionDigits(defMaxDecimals);
    return df.format(valor.multiply(HUNDRED));
  }

  /**
   * Retorna a representação textual completa da taxa de juros com
   * especificação do número de digitos da parte fracionária e uso de
   * acronismo do período de aplicação.
   *
   * @param ndigs O número de digitos da parte fracionária.
   * @param small O indicador de uso de acronismo.
   * @return A representação textual completa da taxa de juros.
  */
  public String toString(int ndigs, boolean small)
  {
    return String.format("%s%% %s %s",
      asString(ndigs),
      periodoStr[small ? 1 : 0][periodo],
      regimeStr[0][regime ? 1 : 0]);
  }

  /**
   * Retorna a representação textual completa da taxa de juros com número
   * especifico de digitos na parte fracionária.
   *
   * @param ndigs O número de digitos da parte fracionária.
   * @return A representação textual completa da taxa de juros com número
   *         especifico de digitos na parte fracionária.
  */
  public String toString(int ndigs)
  {
    return toString(ndigs, false);
  }

  /**
   * Retorna a representação textual completa da taxa de juros com uso de
   * acronismo do período de aplicação.
   *
   * @param O indicador de uso de acronismo.
   * @return A representação textual completa da taxa de juros com uso de
   *         acronismo do período de aplicação.
  */
  public String toString(boolean small)
  {
    return toString(defMaxDecimals, small);
  }

  /**
   * Retorna a representação textual completa da taxa de juros com uso
   * do número default de digitos da parte fracionária e sem uso de
   * acronismo do período de aplicação.
   *
   * @return A representação textual completa da taxa de juros.
  */
  @Override public String toString()
  {
    return toString(defMaxDecimals, false);
  }

  /** Default do número minimo de digitos na formatação. */
  private static final int defMinDecimals = 2;

  /** Default do número maximo de digitos na formatação. */
  private static final int defMaxDecimals = 6;

  /** O número CEM para conveniência de cálculos. */
  public static final BigDecimal HUNDRED = TEN.multiply(TEN);

  /** Tolerancia nas comparações entre números BigDecimal. */
  private static final BigDecimal EPS = new BigDecimal("1E-15", MathContext.DECIMAL64);

  /** Constante lógica indicando uso de regime de capitalização simples. */
  public static final boolean LINEAR_MODEL = true;

  /** Constante lógica indicando uso de regime de capitalização composta. */
  public static final boolean  EXPONENCIAL_MODEL = false;

  /** Constante indicativa de periodicidade da taxa de juros ao dia. */
  public static final int AO_DIA = 0;

  /** Constante indicativa de periodicidade da taxa de juros ao mês. */
  public static final int AO_MES = 1;

  /** Constante indicativa de periodicidade da taxa de juros ao ano. */
  public static final int AO_ANO = 2;

  /** Valor default do número de dias no mês. */
  public static final int DIAS_NO_MES = 30;

  /** Valor default do número de dias no ano. */
  public static final int DIAS_NO_ANO = 360;

  /** Valor padrão imutável do número de meses no ano. */
  public static final int MESES_NO_ANO = 12;

  /**
   * Array das representações textuais de períodos de aplicação de
   * taxas de juros, aonde o primeiro componente contém acronismos
   * dos correspondentes no segundo componente.
  */
  public static final String[][] periodoStr = {
    { "a.d.", "a.m.", "a.a" },
    { "ao dia", "ao mês", "ao ano" }};

  /**
   * Array das representações textuais dos regimes de capitalização.
  */
  public static final String[][] regimeStr = {
    { "exponencial", "linear" },
    { "efetiva", "nominal" },
    { "composta", "simples" }};
}